﻿#region File Description
/****************************************************************************
*                                                                           *
* Game Manager.                                                             *
*                                                                           *
* Copyright (c) Ionescu Marius. All rights reserved(2013).                  *
*                                                                           *
****************************************************************************/
#endregion
namespace Silverlight3D
{
    using System.Windows;
    using System;

    /// <summary>
    /// Rotates 3D coordinates and projects them onto the 2D plane.
    /// </summary>
    public class Projector
    {
        #region Fields

        private const int SineGrain = 1024;

        private double[] Sines;
        private double[] Cosines;

        #endregion

        #region Constructor

        public Projector()
        {
            this.InitDefaultSettings();
            this.InitSines();
        }

        #endregion

        #region Properties

        /// <summary>
        /// Added to all returned 2D coordinates.
        /// </summary>
        public Point Origin { get; set; }

        /// <summary>
        /// View position to use when projecting the 3D coordinates onto the 2D plane.
        /// </summary>
        public Vector ViewPosition { get; set; }

        /// <summary>
        /// How 'near' we can see in the Z direction.  Objects nearer to the view position than this
        /// do not need to be rendered.
        /// </summary>
        public double ViewNearDistance { get; set; }

        /// <summary>
        /// How 'far' we can see in the Z direction.  Objects further away from the view position than this
        /// do not need to be rendered.
        /// </summary>
        public double ViewFarDistance { get; set; }

        /// <summary>
        /// The distance between the human eye and the screen (larger values will reduce the size of the
        /// projected output, as well as reduce the perspective effect).
        /// </summary>
        public double ScreenDistance { get; set; }

        /// <summary>
        /// Every projected 2D coordinate will be multiplied by this.
        /// </summary>
        public double Zoom { get; set; }

        /// <summary>
        /// Angle of rotation about the X axis.
        /// </summary>
        public double RxTheta { get; set; }

        /// <summary>
        /// Angle of rotation about the Y axis.
        /// </summary>
        public double RyTheta { get; set; }

        /// <summary>
        /// Angle of rotation about the Z axis.
        /// </summary>
        public double RzTheta { get; set; }

        /// <summary>
        /// Shading calculation is carried out using this vector as the light source position.
        /// </summary>
        public Vector LightSourcePosition { get; set; }

        #endregion

        #region Private initialisation methods

        private void InitDefaultSettings()
        {
            this.Origin = new Point(0, 0);
            this.ScreenDistance = 900;
        }

        private void InitSines()
        {
            this.Sines = new double[SineGrain];
            this.Cosines = new double[SineGrain];

            for (int i = 0; i < SineGrain; i++)
            {
                this.Sines[i] = Math.Sin(i * 2 * Math.PI / SineGrain);
                this.Cosines[i] = Math.Cos(i * 2 * Math.PI / SineGrain);
            }
        }

        private double Sine(double theta)
        {
            return this.Sines[(int)(theta * SineGrain / (2 * Math.PI)) % SineGrain];
        }

        private double Cosine(double theta)
        {
            return this.Cosines[(int)(theta * SineGrain / (2 * Math.PI)) % SineGrain];
        }

        private Vector RotateXAxis(Vector vector)
        {
            if (RxTheta == 0)
                return vector;

            double rotX = vector.X;
            double rotY = vector.Y * Cosine(RxTheta) - vector.Z * Sine(RxTheta);
            double rotZ = vector.Z * Cosine(RxTheta) + vector.Y * Sine(RxTheta);

            return new Vector(rotX, rotY, rotZ);
        }

        private Vector RotateYAxis(Vector vector)
        {
            if (RyTheta == 0)
                return vector;

            double rotX = vector.X * Cosine(RyTheta) + vector.Z * Sine(RyTheta);
            double rotY = vector.Y;
            double rotZ = -vector.X * Sine(RyTheta) + vector.Z * Cosine(RyTheta);

            return new Vector(rotX, rotY, rotZ);
        }

        private Vector RotateZAxis(Vector vector)
        {
            if (RzTheta == 0)
                return vector;

            double rotX = vector.X * Cosine(RzTheta) - vector.Y * Sine(RzTheta);
            double rotY = vector.Y * Cosine(RzTheta) + vector.X * Sine(RzTheta);
            double rotZ = vector.Z;

            return new Vector(rotX, rotY, rotZ);
        }

        #endregion

        #region Rotation methods

        /// <summary>
        /// Rotates the Vector around the RotationCenter.  Only rotates about the Y axis.
        /// </summary>
        /// <param name="vector">The Vector to rotate.</param>
        /// <returns>The rotated Vector.</returns>
        public Vector Rotate(Vector vector, Vector rotationCenter)
        {
            Vector result = new Vector(vector.X, vector.Y, vector.Z);

            result -= rotationCenter;

            result = this.RotateXAxis(result);
            result = this.RotateYAxis(result);
            result = this.RotateZAxis(result);

            result += rotationCenter;

            return result;
        }

        public VectorLine Rotate(VectorLine vectorLine, Vector rotationCenter)
        {
            if (rotationCenter == null)
            {
                return (VectorLine)vectorLine.Clone();
            }

            VectorLine result = new VectorLine();

            result.Start = this.Rotate(vectorLine.Start, rotationCenter);
            result.End = this.Rotate(vectorLine.End, rotationCenter);

            return result;
        }

        public VectorPolygon Rotate(VectorPolygon vectorPolygon, Vector rotationCenter)
        {
            if (rotationCenter == null)
            {
                return (VectorPolygon)vectorPolygon.Clone();
            }

            VectorPolygon result = new VectorPolygon();

            foreach (var vector in vectorPolygon.Vectors)
            {
                var rotatedVector = this.Rotate(vector, rotationCenter);
                result.Vectors.Add(rotatedVector);
            }

            return result;
        }

        public VectorRect Rotate(VectorRect vectorRect, Vector rotationCenter)
        {
            if (rotationCenter == null)
            {
                return (VectorRect)vectorRect.Clone();
            }

            VectorRect result = new VectorRect();

            result.Vector0 = this.Rotate(vectorRect.Vector0, rotationCenter);
            result.Vector1 = this.Rotate(vectorRect.Vector1, rotationCenter);
            result.Vector3 = this.Rotate(vectorRect.Vector3, rotationCenter);

            return result;
        }

        #endregion

        #region Projection methods

        /// <summary>
        /// Maps a 3D position defined by a Vector to a 2D Point.
        /// </summary>
        /// <param name="vector">3D vector defining a position.</param>
        /// <returns>2D projection of Vector position.</returns>
        public Point? Project(Vector vector, Vector rotationCenter)
        {
            return Project(vector, ViewPosition, rotationCenter);
        }

        /// <summary>
        /// Maps a 3D position defined by x, y and z coordinates to a 2D Point.
        /// Returns NULL if Vector is behind the view position.
        /// </summary>
        /// <param name="x">X position of 3D point.</param>
        /// <param name="y">Y position of 3D point.</param>
        /// <param name="z">Z position of 3D point.</param>
        /// <returns>2D projection of Vector position.</returns>
        public Point? Project(double x, double y, double z, Vector rotationCenter)
        {
            return Project(new Vector() { X = x, Y = y, Z = z }, rotationCenter);
        }

        /// <summary>
        /// Maps a 3D position defined by a Vector to a 2D Point, allowing the view position to be specified.
        /// Returns NULL if Vector is behind the view position.
        /// </summary>
        /// <param name="vector">3D Vector defining a position.</param>
        /// <param name="viewPosition">Current view position as a Vector.</param>
        /// <returns>2D projection of Vector position.</returns>
        public Point? Project(Vector vector, Vector viewPosition, Vector rotationCenter)
        {
            Vector rotatedVector;

            if (rotationCenter == null)
            {
                rotationCenter = new Vector(0, 0, 0);
            }

            if (RxTheta != 0 || RyTheta != 0 || RzTheta != 0)
            {
                rotatedVector = Rotate(vector, rotationCenter);
            }
            else
            {
                rotatedVector = vector;
            }

            if (this.ScreenDistance + rotatedVector.Z < viewPosition.Z)
            {
                return null;
            }

            double projX = ((rotatedVector.X - viewPosition.X) * this.Zoom) / (this.ScreenDistance + rotatedVector.Z - viewPosition.Z) + Origin.X;
            double projY = ((rotatedVector.Y - viewPosition.Y) * this.Zoom) / (this.ScreenDistance + rotatedVector.Z - viewPosition.Z) + Origin.Y;

            return new Point(projX, projY);
        }

        #endregion
    }
}